package com.mrx.commons.aop.service;

import com.alibaba.fastjson2.JSON;
import com.mrx.commons.annotations.XLock;
import com.mrx.commons.aop.enums.LockType;
import com.mrx.commons.aop.exception.LockException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.lang.reflect.Method;

/**
 * 分布式锁 AOP 实现
 *
 * @author Mr.X
 * @since 2023-11-15 17:11
 **/
@Aspect
@Component
public class LockAop {

    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    private static final String REDIS_LOCK_KEY = "x-lock:";

    private static final Logger logger = LoggerFactory.getLogger(LockAop.class);

    private static final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
    private static final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();

    @Pointcut("@annotation(com.mrx.commons.annotations.XLock)")
    public void lockPoint() {
    }

    @Around("lockPoint()")
    @SuppressWarnings("BusyWait")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        XLock lock = method.getAnnotation(XLock.class);
        String key = parseKey(lock.value(), method, point.getArgs());
        long startTime = System.currentTimeMillis();
        logger.info("准备加锁: {}", key);
        int i = 0;
        for (; i < lock.retryTimes(); i++) {
            if (lock(key, lock)) {
                logger.info("{} 加锁成功", key);
                break;
            } else {
                // 等待超时
                if (lock.maxWaitTime() + startTime > System.currentTimeMillis()) {
                    fail(lock);
                }
                logger.info("{} 已经上锁, 等待中", key);
                Thread.sleep(Math.max(lock.sleepMills(), 10));
            }
        }
        // 若等待 retryTimes 后还未能成功获取锁, 则抛出异常
        if (i >= lock.retryTimes()) {
            fail(lock);
        }
        try {
            return point.proceed();
        } finally {
            releaseLock(key);
        }
    }

    private boolean lock(String key, XLock lock) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY + key, 1);
        boolean succeed = Boolean.TRUE.equals(result);
        if (!succeed && lock.lockType().equals(LockType.MUTEX_LOCK)) {
            fail(lock);
        }
        return succeed;
    }

    private void releaseLock(String key) {
        redisTemplate.delete(REDIS_LOCK_KEY + key);
    }

    private String parseKey(String conditions, Method method, Object[] arguments) {
        if (conditions.isEmpty()) {
            throw new IllegalArgumentException("加锁的 key 不能为空");
        }
        Expression expression = spelExpressionParser.parseExpression(conditions);
        StandardEvaluationContext context = new StandardEvaluationContext();
        String[] parameterNames = nameDiscoverer.getParameterNames(method);
        if (parameterNames == null) {
            throw new IllegalStateException("Parameter names is null");
        }
        for (int i = 0; i < parameterNames.length; ++i) {
            context.setVariable(parameterNames[i], arguments[i]);
        }
        return JSON.toJSONString(expression.getValue(context));
    }

    private void fail(XLock lock) {
        String message = lock.lockedMessage();
        throw new LockException(StringUtils.hasText(message) ? message : null);
    }

}
